import Combine
import EvmKit
import Foundation
import HsToolKit
import RxRelay
import RxSwift
import WalletConnectSign
import WalletConnectUtils

class WalletConnectMainService {
    private let disposeBag = DisposeBag()
    private var cancellables = Set<AnyCancellable>()

    private let service: WalletConnectService
    private let reachabilityManager: IReachabilityManager
    private let purchaseManager = Core.shared.purchaseManager
    private let dappProvider = WhitelistDappProvider(networkManager: Core.shared.networkManager)
    private let accountManager: AccountManager
    private let proposalHandler: IProposalHandler

    private var proposal: WalletConnectSign.Session.Proposal?
    private(set) var session: WalletConnectSign.Session? {
        didSet {
            sessionUpdatedRelay.accept(session)
        }
    }

    private(set) var premiumEnabled: Bool {
        didSet {
            syncWhitelist(url: proposal?.proposer.url ?? session?.peer.url)
        }
    }

    private let connectionStateRelay = PublishRelay<WalletConnectMainModule.ConnectionState>()
    private let requestRelay = PublishRelay<WalletConnectSign.Request>()
    private let errorRelay = PublishRelay<Error>()
    private let connectedRelay = PublishRelay<Void>()
    private let sessionUpdatedRelay = PublishRelay<WalletConnectSign.Session?>()

    private var whitelistDappState: WhitelistDappState = .loading
    private let whitelistStateRelay = PublishRelay<WalletConnectMainModule.WhitelistState>()
    private(set) var whitelistState: WalletConnectMainModule.WhitelistState = .loading {
        didSet {
            whitelistStateRelay.accept(whitelistState)
        }
    }

    private let allowedBlockchainsRelay = PublishRelay<[WalletConnectMainModule.BlockchainProposal]>()

    private var blockchains = [WalletConnectMainModule.BlockchainProposal]()

    private let stateRelay = PublishRelay<WalletConnectMainModule.State>()
    private(set) var state: WalletConnectMainModule.State = .idle {
        didSet {
            stateRelay.accept(state)
        }
    }

    init(session: WalletConnectSign.Session? = nil, proposal: WalletConnectSign.Session.Proposal? = nil, service: WalletConnectService, reachabilityManager: IReachabilityManager, accountManager: AccountManager, proposalHandler: IProposalHandler) {
        self.session = session
        self.proposal = proposal
        self.service = service
        self.reachabilityManager = reachabilityManager
        self.accountManager = accountManager
        self.proposalHandler = proposalHandler

        premiumEnabled = purchaseManager.activated(.vipSupport)
        loadWhitelist()

        subscribe(disposeBag, service.receiveProposalObservable) { [weak self] in
            self?.proposal = $0
            self?.sync(proposal: $0)
        }
        subscribe(disposeBag, service.receiveSessionObservable) { [weak self] in
            self?.session = $0
            self?.didReceive(session: $0)
        }
        subscribe(disposeBag, service.deleteSessionObservable) { [weak self] in
            self?.didDelete(topic: $0, reason: $1)
        }
        subscribe(disposeBag, service.socketConnectionStatusObservable) { [weak self] in
            self?.connectionStateRelay.accept($0)
        }

        connectionStateRelay.accept(service.socketConnectionStatus == .connected ? .connected : .disconnected)

        purchaseManager.$activeFeatures
            .sink { [weak self] features in
                self?.premiumEnabled = features.contains(.vipSupport)
            }
            .store(in: &cancellables)

        if let session {
            didReceive(session: session)
        }

        if let proposal {
            sync(proposal: proposal)
        }
    }

    private func loadWhitelist() {
        dappProvider.whitelistDapps()
            .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
            .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
            .subscribe(
                onSuccess: { [weak self] dApps in
                    self?.updateWhitelistDapps(dApps: dApps)
                },
                onError: { [weak self] _ in
                    self?.updateWhitelistDapps(dApps: nil)
                }
            )

            .disposed(by: disposeBag)
    }

    private func updateWhitelistDapps(dApps: [WhitelistDapp]?) {
        if let dApps {
            whitelistDappState = .loaded(dApps)
        } else {
            whitelistDappState = .error
        }

        syncWhitelist(url: proposal?.proposer.url ?? session?.peer.url)
    }

    private func sync(proposal: WalletConnectSign.Session.Proposal) {
        syncWhitelist(url: proposal.proposer.url)
        do {
            let blockchains = proposalHandler.handle(provider: proposal)
            try ProposalValidator.validate(namespaces: proposal.requiredNamespaces, blockchains: blockchains)

            self.blockchains = blockchains
            allowedBlockchainsRelay.accept(allowedBlockchains)

            guard !blockchains.isEmpty else {
                state = .invalid(error: WalletConnectMainModule.SessionError.noAnySupportedChainId)
                return
            }

            state = .waitingForApproveSession
        } catch {
            state = .invalid(error: error)
            return
        }
    }

    private func didReceive(session: WalletConnectSign.Session) {
        syncWhitelist(url: session.peer.url)

        do {
            let blockchains = proposalHandler.handle(provider: session)
            try ProposalValidator.validate(namespaces: session.proposalNamespaces, blockchains: blockchains)

            self.blockchains = blockchains
            allowedBlockchainsRelay.accept(allowedBlockchains)

            state = .ready
        } catch {
            state = .invalid(error: WalletConnectMainModule.SessionError.noAnySupportedChainId)
            return
        }
    }

    private func syncWhitelist(url: String?) {
        guard let url else {
            whitelistState = .notAvailable
            return
        }

        switch whitelistDappState {
        case .loading: whitelistState = .loading
        case .error: whitelistState = .notAvailable
        case let .loaded(dApps):
            guard let urlComponents = URLComponents(string: url), let host = urlComponents.host else {
                whitelistState = .risky
                return
            }

            let contains = dApps.first { dApp in
                host.lowercased().hasSuffix(dApp.url.lowercased())
            }

            whitelistState = contains != nil ? .secure : .risky
        }
    }

    private func didDelete(topic: String, reason _: WalletConnectSign.Reason) {
        guard let currentTopic = session?.topic, currentTopic == topic else {
            return
        }

        state = .killed(reason: .killSession) // TODO: ???
    }
}

extension WalletConnectMainService {
    var activeAccountName: String? {
        accountManager.activeAccount?.name
    }

    var appMetaItem: WalletConnectMainModule.AppMetaItem? {
        if let session {
            return WalletConnectMainModule.AppMetaItem(
                name: session.peer.name,
                url: session.peer.url,
                description: session.peer.description,
                icons: session.peer.icons
            )
        }
        if let proposal {
            return WalletConnectMainModule.AppMetaItem(
                name: proposal.proposer.name,
                url: proposal.proposer.url,
                description: proposal.proposer.description,
                icons: proposal.proposer.icons
            )
        }

        return nil
    }

    var allowedBlockchains: [WalletConnectMainModule.BlockchainProposal] {
        blockchains.sorted { blockchain, blockchain2 in
            blockchain.item.chainId < blockchain2.item.chainId
        }
    }

    var hint: String? {
        switch connectionState {
        case .disconnected:
            if state == .waitingForApproveSession || state == .ready {
                return "wallet_connect.no_connection".localized
            }
        case .connecting: return nil
        case .connected: ()
        }

        switch state {
        case let .invalid(error):
            return error.smartDescription
        case .waitingForApproveSession:
            return "wallet_connect.connect_description".localized
        default:
            return nil
        }
    }

    var stateObservable: Observable<WalletConnectMainModule.State> {
        stateRelay.asObservable()
    }

    var whitelistStateObservable: Observable<WalletConnectMainModule.WhitelistState> {
        whitelistStateRelay.asObservable()
    }

    var sessionUpdatedObservable: Observable<WalletConnectSign.Session?> {
        sessionUpdatedRelay.asObservable()
    }

    var connectionState: WalletConnectMainModule.ConnectionState {
        service.socketConnectionStatus
    }

    var connectionStateObservable: Observable<WalletConnectMainModule.ConnectionState> {
        connectionStateRelay.asObservable()
    }

    var proposalTimeOutAttentionObservable: Observable<Void> {
        Observable.empty()
    }

    var errorObservable: Observable<Error> {
        errorRelay.asObservable()
    }

    var connectedObservable: Observable<Void> {
        connectedRelay.asObservable()
    }

    var allowedBlockchainsObservable: Observable<[WalletConnectMainModule.BlockchainProposal]> {
        allowedBlockchainsRelay.asObservable()
    }

    func reconnect() {
        guard session != nil else {
            return
        }

        guard reachabilityManager.isReachable else {
            errorRelay.accept(AppError.noConnection)
            return
        }
    }

    func approveSession() {
        guard let proposal else {
            return
        }

        guard reachabilityManager.isReachable else {
            errorRelay.accept(AppError.noConnection)
            return
        }

        guard accountManager.activeAccount != nil else {
            state = .invalid(error: WalletConnectMainModule.SessionError.noSuitableAccount)
            return
        }

        Task { [weak self, service, blockchains] in
            do {
                try await service.approve(proposal: proposal, blockchains: blockchains)
                self?.connectedRelay.accept(())
            } catch {
                self?.errorRelay.accept(error)
            }
        }
    }

    func rejectSession() {
        guard reachabilityManager.isReachable else {
            errorRelay.accept(AppError.noConnection)
            return
        }

        if let proposal {
            Task { [weak self, service] in
                defer {
                    self?.state = .killed(reason: .rejectProposal)
                }
                do {
                    try await service.reject(proposal: proposal)
                } catch {
                    self?.errorRelay.accept(error)
                }
            }
        }
    }

    func killSession() {
        guard reachabilityManager.isReachable else {
            errorRelay.accept(AppError.noConnection)
            return
        }

        guard let session else {
            return
        }

        service.disconnect(topic: session.topic, reason: RejectionReason(code: 1, message: "Session Killed by User"))
        state = .killed(reason: .killSession) // TODO: ???
    }
}

extension WalletConnectMainService {
    struct RejectionReason: Reason {
        let code: Int
        let message: String
    }

    struct SessionData {
        let proposal: WalletConnectSign.Session.Proposal
        let appMeta: WalletConnectMainModule.AppMetaItem
    }

    enum WhitelistDappState: Equatable {
        case loading
        case error
        case loaded([WhitelistDapp])

        static func == (lhs: Self, rhs: Self) -> Bool {
            switch (lhs, rhs) {
            case (.loading, .loading): return true
            case (.error, .error): return true
            case let (.loaded(lhsApps), .loaded(rhsApps)): return Set(lhsApps) == Set(rhsApps)
            default: return false
            }
        }
    }
}
